#ifdef  BIT_ENDBG
#define DBG(...)		printf( __VA_ARGS__ )
#else
#define DBG(...)
#endif

#define	LEN(a, t)	sizeof(a)/sizeof(t)


/* getopt */
#include <unistd.h>
/* open */
#include <sys/stat.h>
#include <fcntl.h>
/* errno */
#include <errno.h>
/* mmap */
#include <sys/mman.h>
/* clock */
#include <time.h>
/* shmem */
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>


#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

#include "bitonic_interface.h"


void usage(int argc, char *argv[]) {
	printf(
"Usage %s [-f file_in | -s shm_in -l len] [-h] [-t]\n\
-f : use file as input\n\
-s : use shared memory as input, requires -l\n\
-l : size of the shared memory in data_t's\n\
-h : help\n\
-t : short self test\n\
", argv[0]);
}

size_t find_m(data_t data[], size_t l) {
	for(size_t i = 1; i < l; i++) {
		if(data[i-1] > data[i]) {
			return (i-1);
		}
	}
	return l-1;
}

void print_array(data_t *a, size_t l) {
	printf("\n");
	for(size_t i = 0; i < l; i++)
		printf("  %x  ", a[i]);
	printf("\n");
	for(size_t i = 0;i < l; i++)
		printf(" [%ld] ", i);
	printf("\n");
}

int is_valid_input(data_t *in, size_t l) {
	size_t i = 1;

	for(; i < l; i++) {
		if(in[i-1] > in[i]) {
			break;
		}
	}
	i++;
	for(; i < l; i++) {
		if(in[i-1] > in[i]) {
			return 0;
		}
	}
	return 1;
}

int is_sorted(data_t *in, size_t l) {
	for(size_t i = 1; i < l; i++) {
		if(in[i-1] > in[i]) {
			return 0;
		}
	}
	return 1;
}

void run_test(data_t *in, size_t l) {
	print_array(in, l);
	size_t m = find_m(in, l);
	bitonic_merge(in, m, l, 4);
	print_array(in, l);

	if(!is_sorted(in, l)) {
		printf("Test failed.\n");
		print_array(in, l);
	}
}

void self_test(void) {
	data_t test1[8] = {1, 2, 3, 4, 2, 3, 4, 5};
	data_t test2[8] = {1, 2, 1, 2, 3, 4, 5, 6};
	data_t test3[8] = {1, 2, 3, 4, 5, 6, 1, 2};
	data_t test4[8] = {1, 2, 3, 4, 5, 6, 7, 8};
	data_t test5[9] = {1, 2, 3, 4, 5, 6, 2, 3, 4};
	data_t test6[9] = {1, 2, 3, 4, 5, 1, 2, 3, 4};
	data_t test7[9] = {1, 2, 3, 1, 2, 3, 4, 5, 6};

	printf("Starting self Tests ..\n");
	/*run_test(test1, LEN(test1, data_t));
	run_test(test2, LEN(test2, data_t));*/
	run_test(test3, LEN(test3, data_t));
	//run_test(test4, LEN(test4, data_t));
	/*run_test(test5, LEN(test5, data_t));
	run_test(test6, LEN(test6, data_t));
	run_test(test7, LEN(test7, data_t));*/
	printf("Finished self Tests ..\n");
}

int main(int argc, char *argv[]) {
	struct opt_flags {
		uint8_t shm;
		uint8_t l;
	} flags = {0, 0};
	int opt;
	int in_fd = -1;
	struct stat in_stats;
	size_t l_bytes;
	size_t l_data_t = 0;
	int p = 0;
	data_t *data_in;

	int status = 0;

	while((opt = getopt(argc, argv, "hs:l:tf:p:")) != -1) {
		switch(opt) {
			case 't' : {
				self_test();
				break;
			}
			case 'p' : {
				p = strtol(optarg, NULL, 10);
				if(!((p > 0) && !(p & (p - 1)))) {
					printf("p is required to be a power of 2 (%d)\n", p);
					return 1;
				}
				break;
			}
			case 'f' : {
				if(in_fd != -1) {
					printf("You mixed -f with -s arguments.\n");
					return 1;
				}
				if((in_fd = open(optarg, O_RDONLY)) == -1) {
					printf("%s\n",
						strerror(errno));
					return 1;
				}
				/* Get size */
				if(stat(optarg, &in_stats) == -1) {
					printf("%s\n",
						strerror(errno));
					return 1;
				}
				l_bytes = in_stats.st_size;
				l_data_t = l_bytes/sizeof(data_t);
				DBG("In file size: %ld\n", l_bytes);
				DBG("In data count: %ld\n", l_data_t);
				/* Get file map */
				/*data_in = mmap(NULL,
					l_bytes,
					PROT_READ | PROT_WRITE,
					MAP_PRIVATE,
					in_fd,
					0);
				if(MAP_FAILED == data_in) {
					printf("%s\n",
						strerror(errno));
					return 1;
				}*/
				break;
			}
			case 's' : {
				if(in_fd != -1) {
					printf("You mixed -f with -s arguments.\n");
					return 1;
				}
				DBG("Opening shm: %s\n", optarg);
				in_fd = shm_open(optarg,
					O_RDWR,
					S_IRWXU);
				if(in_fd == -1) {
					printf("%s\n",
						strerror(errno));
					return 1;
				}
				flags.shm = 1;
				break;
			}
			case 'l' : {
				/* TODO this can conflict when -f and -l is given. */
				l_data_t = strtol(optarg, NULL, 10);
				l_bytes = l_data_t*sizeof(data_t);
				flags.l = 1;
				break;
			}
			case 'h' :
			default : {
				usage(argc, argv);
			}
		}
	}

	if((flags.shm == 1) && (flags.l != 1)) {
		printf("-s requires a -l argument.\n");
		usage(argc, argv);
		return 1;
	}

	if(in_fd != -1) {
		data_in = mmap(NULL,
			l_bytes,
			PROT_READ | PROT_WRITE,
			MAP_PRIVATE,
			in_fd,
			0);
		if(MAP_FAILED == data_in) {
			printf("%s\n",
				strerror(errno));
			return 1;
		}

		//print_array(data_in, l_data_t);

		if(is_valid_input(data_in, l_data_t)) {
			if(is_sorted(data_in, l_data_t)) {
				printf("Input sequence is already sorted!\n");
				return 1;
			}
			size_t m = find_m(data_in, l_data_t);
			DBG("m[%ld] l[%ld]\n", m, l_data_t);

			struct timespec cpu_time;
			struct timespec real_time;

			clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &cpu_time);
			clock_gettime(CLOCK_REALTIME, &real_time);

			time_t start_sec_cpu = cpu_time.tv_sec;
			long   start_nsec_cpu = cpu_time.tv_nsec;
			time_t start_sec_real = real_time.tv_sec;
			long   start_nsec_real = real_time.tv_nsec;

			bitonic_merge(data_in, m, l_data_t, p);

			clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &cpu_time);
			clock_gettime(CLOCK_REALTIME, &real_time);

			long cpu_nsec = cpu_time.tv_nsec-start_nsec_cpu;
			long real_nsec = real_time.tv_nsec-start_nsec_real;

			time_t cpu_sec = cpu_time.tv_sec-start_sec_cpu;
			time_t real_sec = real_time.tv_sec-start_sec_real;

			if(start_nsec_cpu > cpu_time.tv_nsec) {
				cpu_sec--;
				cpu_nsec = 1000000000 - start_nsec_cpu + cpu_time.tv_nsec;
			}

			if(start_nsec_real > real_time.tv_nsec) {
				real_sec--;
				real_nsec = 1000000000 - start_nsec_real + real_time.tv_nsec;
			}

			printf(" cpu  %ld.%09ld\n", cpu_sec, cpu_nsec);
			printf(" real %ld.%09lu\n", real_sec, real_nsec);

			//printf(" %d\n", is_sorted(data_in, l_data_t));
			status = ! is_sorted(data_in, l_data_t);
		} else {
			printf("Invalid input.\n");
		}
		munmap(data_in, l_bytes);
	}
	return status;
}
